@hypothesi/tauri-mcp-server 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +39 -0
- package/dist/config.js +45 -0
- package/dist/driver/app-discovery.js +148 -0
- package/dist/driver/plugin-client.js +208 -0
- package/dist/driver/plugin-commands.js +142 -0
- package/dist/driver/protocol.js +7 -0
- package/dist/driver/scripts/find-element.js +48 -0
- package/dist/driver/scripts/focus.js +17 -0
- package/dist/driver/scripts/get-styles.js +41 -0
- package/dist/driver/scripts/html2canvas-loader.js +86 -0
- package/dist/driver/scripts/index.js +94 -0
- package/dist/driver/scripts/interact.js +103 -0
- package/dist/driver/scripts/keyboard.js +76 -0
- package/dist/driver/scripts/swipe.js +88 -0
- package/dist/driver/scripts/wait-for.js +44 -0
- package/dist/driver/session-manager.js +121 -0
- package/dist/driver/webview-executor.js +334 -0
- package/dist/driver/webview-interactions.js +213 -0
- package/dist/index.js +80 -0
- package/dist/manager/cli.js +128 -0
- package/dist/manager/config.js +142 -0
- package/dist/manager/docs.js +213 -0
- package/dist/manager/mobile.js +83 -0
- package/dist/monitor/logs.js +98 -0
- package/dist/tools-registry.js +292 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Fireside Development, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Apache License, Version 2.0
|
|
26
|
+
|
|
27
|
+
Copyright (c) 2025 Fireside Development, LLC
|
|
28
|
+
|
|
29
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
30
|
+
you may not use this file except in compliance with the License.
|
|
31
|
+
You may obtain a copy of the License at
|
|
32
|
+
|
|
33
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
34
|
+
|
|
35
|
+
Unless required by applicable law or agreed to in writing, software
|
|
36
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
37
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
38
|
+
See the License for the specific language governing permissions and
|
|
39
|
+
limitations under the License.
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the MCP Bridge connection.
|
|
3
|
+
*
|
|
4
|
+
* This module provides configuration options for connecting to Tauri apps,
|
|
5
|
+
* with support for environment variables and sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Gets the default host for MCP Bridge connections.
|
|
9
|
+
*
|
|
10
|
+
* Resolution priority:
|
|
11
|
+
* 1. MCP_BRIDGE_HOST environment variable
|
|
12
|
+
* 2. TAURI_DEV_HOST environment variable (set by Tauri CLI for mobile dev)
|
|
13
|
+
* 3. 'localhost' (default)
|
|
14
|
+
*/
|
|
15
|
+
export function getDefaultHost() {
|
|
16
|
+
// eslint-disable-next-line no-process-env
|
|
17
|
+
return process.env.MCP_BRIDGE_HOST || process.env.TAURI_DEV_HOST || 'localhost';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Gets the default port for MCP Bridge connections.
|
|
21
|
+
*
|
|
22
|
+
* Resolution priority:
|
|
23
|
+
* 1. MCP_BRIDGE_PORT environment variable
|
|
24
|
+
* 2. 9223 (default)
|
|
25
|
+
*/
|
|
26
|
+
export function getDefaultPort() {
|
|
27
|
+
// eslint-disable-next-line no-process-env
|
|
28
|
+
const port = process.env.MCP_BRIDGE_PORT;
|
|
29
|
+
return port ? parseInt(port, 10) : 9223;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets the full bridge configuration from environment variables.
|
|
33
|
+
*/
|
|
34
|
+
export function getConfig() {
|
|
35
|
+
return {
|
|
36
|
+
host: getDefaultHost(),
|
|
37
|
+
port: getDefaultPort(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Builds a WebSocket URL from host and port.
|
|
42
|
+
*/
|
|
43
|
+
export function buildWebSocketURL(host, port) {
|
|
44
|
+
return `ws://${host}:${port}`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App discovery and session management for multiple Tauri instances.
|
|
3
|
+
*
|
|
4
|
+
* This module handles discovering and connecting to multiple Tauri apps
|
|
5
|
+
* running with MCP Bridge on the same machine or remote devices using port scanning.
|
|
6
|
+
*/
|
|
7
|
+
import { getDefaultHost, getDefaultPort } from '../config.js';
|
|
8
|
+
import { PluginClient } from './plugin-client.js';
|
|
9
|
+
/**
|
|
10
|
+
* Manages discovery and connection to multiple Tauri app instances
|
|
11
|
+
*/
|
|
12
|
+
export class AppDiscovery {
|
|
13
|
+
_activeSessions = new Map();
|
|
14
|
+
_host;
|
|
15
|
+
_basePort;
|
|
16
|
+
_maxPorts = 100;
|
|
17
|
+
constructor(host, basePort) {
|
|
18
|
+
this._host = host ?? getDefaultHost();
|
|
19
|
+
this._basePort = basePort ?? getDefaultPort();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the configured host.
|
|
23
|
+
*/
|
|
24
|
+
get host() {
|
|
25
|
+
return this._host;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sets the host for discovery.
|
|
29
|
+
*/
|
|
30
|
+
setHost(host) {
|
|
31
|
+
this._host = host;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discovers available Tauri app instances by scanning ports
|
|
35
|
+
*/
|
|
36
|
+
async discoverApps() {
|
|
37
|
+
const apps = [];
|
|
38
|
+
// Scan port range for available apps
|
|
39
|
+
for (let offset = 0; offset < this._maxPorts; offset++) {
|
|
40
|
+
const port = this._basePort + offset;
|
|
41
|
+
if (await this._isPortInUse(port)) {
|
|
42
|
+
apps.push({ host: this._host, port, available: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return apps;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Connects to a specific app on a host and port
|
|
49
|
+
*/
|
|
50
|
+
async connectToPort(port, appName, host) {
|
|
51
|
+
const targetHost = host ?? this._host;
|
|
52
|
+
const sessionId = `${targetHost}_${port}`;
|
|
53
|
+
// Check if already connected
|
|
54
|
+
const existing = this._activeSessions.get(sessionId);
|
|
55
|
+
if (existing?.connected) {
|
|
56
|
+
return existing;
|
|
57
|
+
}
|
|
58
|
+
const client = new PluginClient(targetHost, port);
|
|
59
|
+
try {
|
|
60
|
+
await client.connect();
|
|
61
|
+
const session = {
|
|
62
|
+
appId: sessionId,
|
|
63
|
+
name: appName || `Tauri App (${targetHost}:${port})`,
|
|
64
|
+
host: targetHost,
|
|
65
|
+
port,
|
|
66
|
+
client,
|
|
67
|
+
connected: true,
|
|
68
|
+
};
|
|
69
|
+
this._activeSessions.set(sessionId, session);
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(`Failed to connect to ${targetHost}:${port}: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Gets the first available app
|
|
78
|
+
*/
|
|
79
|
+
async getFirstAvailableApp() {
|
|
80
|
+
const apps = await this.discoverApps();
|
|
81
|
+
return apps.length > 0 ? apps[0] : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Disconnects from a specific session
|
|
85
|
+
*/
|
|
86
|
+
async disconnectSession(sessionId) {
|
|
87
|
+
const session = this._activeSessions.get(sessionId);
|
|
88
|
+
if (session?.client) {
|
|
89
|
+
await session.client.disconnect();
|
|
90
|
+
this._activeSessions.delete(sessionId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Disconnects from all apps
|
|
95
|
+
*/
|
|
96
|
+
async disconnectAll() {
|
|
97
|
+
for (const [, session] of this._activeSessions) {
|
|
98
|
+
if (session.client) {
|
|
99
|
+
await session.client.disconnect();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this._activeSessions.clear();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gets the active session by ID
|
|
106
|
+
*/
|
|
107
|
+
getSession(sessionId) {
|
|
108
|
+
return this._activeSessions.get(sessionId);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Gets all active sessions
|
|
112
|
+
*/
|
|
113
|
+
getAllSessions() {
|
|
114
|
+
return Array.from(this._activeSessions.values());
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Try to connect to the default port
|
|
118
|
+
*/
|
|
119
|
+
async connectToDefaultPort() {
|
|
120
|
+
return this.connectToPort(this._basePort, 'Default Tauri App');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if a port is in use (likely a Tauri app)
|
|
124
|
+
*/
|
|
125
|
+
async _isPortInUse(port) {
|
|
126
|
+
const client = new PluginClient(this._host, port);
|
|
127
|
+
try {
|
|
128
|
+
// Try to connect briefly to see if port responds
|
|
129
|
+
await Promise.race([
|
|
130
|
+
client.connect(),
|
|
131
|
+
new Promise((_, reject) => {
|
|
132
|
+
setTimeout(() => { reject(new Error('Timeout')); }, 100);
|
|
133
|
+
}),
|
|
134
|
+
]);
|
|
135
|
+
// Connection succeeded - clean up and return true
|
|
136
|
+
client.disconnect();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Connection failed or timed out - always clean up the client
|
|
141
|
+
// This prevents orphaned WebSocket connections from emitting errors later
|
|
142
|
+
client.disconnect();
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Singleton instance
|
|
148
|
+
export const appDiscovery = new AppDiscovery();
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { buildWebSocketURL, getDefaultHost, getDefaultPort } from '../config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Client to communicate with the MCP Bridge plugin's WebSocket server
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable no-plusplus */
|
|
8
|
+
export class PluginClient extends EventEmitter {
|
|
9
|
+
_ws = null;
|
|
10
|
+
_url;
|
|
11
|
+
_host;
|
|
12
|
+
_port;
|
|
13
|
+
_reconnectAttempts = 0;
|
|
14
|
+
_shouldReconnect = true; // Keep trying forever until explicitly disconnected
|
|
15
|
+
_reconnectDelay = 1000; // Start with 1s, max 30s
|
|
16
|
+
_pendingRequests = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Constructor for PluginClient
|
|
19
|
+
* @param host Host address of the WebSocket server
|
|
20
|
+
* @param port Port number of the WebSocket server
|
|
21
|
+
*/
|
|
22
|
+
constructor(host, port) {
|
|
23
|
+
super();
|
|
24
|
+
this._host = host;
|
|
25
|
+
this._port = port;
|
|
26
|
+
this._url = buildWebSocketURL(host, port);
|
|
27
|
+
// CRITICAL: Attach a default error handler to prevent crashes.
|
|
28
|
+
// In Node.js, if an EventEmitter emits 'error' with no listeners, it throws
|
|
29
|
+
// an uncaught exception that crashes the process. This is especially important
|
|
30
|
+
// during port scanning where connections may fail after the caller has moved on.
|
|
31
|
+
this.on('error', () => {
|
|
32
|
+
// Silently ignore - errors are also returned via promise rejections
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a PluginClient with default configuration from environment.
|
|
37
|
+
*/
|
|
38
|
+
static create_default() {
|
|
39
|
+
return new PluginClient(getDefaultHost(), getDefaultPort());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Gets the host this client is configured to connect to.
|
|
43
|
+
*/
|
|
44
|
+
get host() {
|
|
45
|
+
return this._host;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Gets the port this client is configured to connect to.
|
|
49
|
+
*/
|
|
50
|
+
get port() {
|
|
51
|
+
return this._port;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connect to the plugin's WebSocket server
|
|
55
|
+
*/
|
|
56
|
+
async connect() {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
if (this._ws?.readyState === WebSocket.OPEN) {
|
|
59
|
+
resolve();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this._ws = new WebSocket(this._url);
|
|
63
|
+
this._ws.on('open', () => {
|
|
64
|
+
// Connected to MCP Bridge plugin
|
|
65
|
+
this._reconnectAttempts = 0;
|
|
66
|
+
this.emit('connected');
|
|
67
|
+
resolve();
|
|
68
|
+
});
|
|
69
|
+
this._ws.on('message', (data) => {
|
|
70
|
+
try {
|
|
71
|
+
const message = JSON.parse(data.toString());
|
|
72
|
+
// Check if this is a response to a pending request
|
|
73
|
+
if (message.id && this._pendingRequests.has(message.id)) {
|
|
74
|
+
const pending = this._pendingRequests.get(message.id);
|
|
75
|
+
if (pending) {
|
|
76
|
+
clearTimeout(pending.timeout);
|
|
77
|
+
this._pendingRequests.delete(message.id);
|
|
78
|
+
pending.resolve(message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// It's a broadcast event
|
|
83
|
+
this.emit('event', message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
// Failed to parse WebSocket message
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
this._ws.on('error', (err) => {
|
|
91
|
+
// WebSocket error - emit for any listeners, then reject the promise.
|
|
92
|
+
// Note: The constructor attaches a default error handler to prevent crashes.
|
|
93
|
+
this.emit('error', err);
|
|
94
|
+
reject(err);
|
|
95
|
+
});
|
|
96
|
+
this._ws.on('close', () => {
|
|
97
|
+
// Disconnected from MCP Bridge plugin
|
|
98
|
+
this.emit('disconnected');
|
|
99
|
+
this._ws = null;
|
|
100
|
+
// Reject all pending requests since the connection is gone
|
|
101
|
+
for (const [id, pending] of this._pendingRequests) {
|
|
102
|
+
clearTimeout(pending.timeout);
|
|
103
|
+
pending.reject(new Error('Connection closed'));
|
|
104
|
+
this._pendingRequests.delete(id);
|
|
105
|
+
}
|
|
106
|
+
// Auto-reconnect with exponential backoff (max 30s)
|
|
107
|
+
if (this._shouldReconnect) {
|
|
108
|
+
this._reconnectAttempts++;
|
|
109
|
+
const delay = Math.min(this._reconnectDelay * this._reconnectAttempts, 30000);
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
this.connect().catch(() => {
|
|
112
|
+
// Reconnection failed - will retry on next close event
|
|
113
|
+
});
|
|
114
|
+
}, delay);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Disconnect from the plugin
|
|
121
|
+
*/
|
|
122
|
+
disconnect() {
|
|
123
|
+
this._shouldReconnect = false; // Prevent auto-reconnect
|
|
124
|
+
if (this._ws) {
|
|
125
|
+
this._ws.close();
|
|
126
|
+
this._ws = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Send a command to the plugin and wait for response
|
|
131
|
+
*/
|
|
132
|
+
async sendCommand(command, timeoutMs = 5000) {
|
|
133
|
+
// If not connected, try to reconnect first
|
|
134
|
+
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
135
|
+
try {
|
|
136
|
+
await this.connect();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error('Not connected to plugin and reconnection failed');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Double-check connection after reconnect attempt
|
|
143
|
+
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
144
|
+
throw new Error('Not connected to plugin');
|
|
145
|
+
}
|
|
146
|
+
// Generate unique ID for this request
|
|
147
|
+
const id = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
148
|
+
const commandWithId = { ...command, id };
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
// Set up timeout
|
|
151
|
+
const timeout = setTimeout(() => {
|
|
152
|
+
this._pendingRequests.delete(id);
|
|
153
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
154
|
+
}, timeoutMs);
|
|
155
|
+
// Store pending request
|
|
156
|
+
this._pendingRequests.set(id, { resolve, reject, timeout });
|
|
157
|
+
// Send command
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
159
|
+
this._ws.send(JSON.stringify(commandWithId), (error) => {
|
|
160
|
+
if (error) {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
this._pendingRequests.delete(id);
|
|
163
|
+
reject(error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if connected
|
|
170
|
+
*/
|
|
171
|
+
isConnected() {
|
|
172
|
+
return this._ws?.readyState === WebSocket.OPEN;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Singleton instance
|
|
176
|
+
let pluginClient = null;
|
|
177
|
+
/**
|
|
178
|
+
* Gets or creates a singleton PluginClient.
|
|
179
|
+
* @param host Optional host override
|
|
180
|
+
* @param port Optional port override
|
|
181
|
+
*/
|
|
182
|
+
export function getPluginClient(host, port) {
|
|
183
|
+
const resolvedHost = host ?? getDefaultHost();
|
|
184
|
+
const resolvedPort = port ?? getDefaultPort();
|
|
185
|
+
if (!pluginClient) {
|
|
186
|
+
pluginClient = new PluginClient(resolvedHost, resolvedPort);
|
|
187
|
+
}
|
|
188
|
+
return pluginClient;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Resets the singleton client (useful for reconnecting with different config).
|
|
192
|
+
*/
|
|
193
|
+
export function resetPluginClient() {
|
|
194
|
+
if (pluginClient) {
|
|
195
|
+
pluginClient.disconnect();
|
|
196
|
+
pluginClient = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export async function connectPlugin(host, port) {
|
|
200
|
+
const client = getPluginClient(host, port);
|
|
201
|
+
if (!client.isConnected()) {
|
|
202
|
+
await client.connect();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function disconnectPlugin() {
|
|
206
|
+
const client = getPluginClient();
|
|
207
|
+
client.disconnect();
|
|
208
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
3
|
+
export const ExecuteIPCCommandSchema = z.object({
|
|
4
|
+
command: z.string(),
|
|
5
|
+
args: z.unknown().optional(),
|
|
6
|
+
});
|
|
7
|
+
export async function executeIPCCommand(command, args = {}) {
|
|
8
|
+
try {
|
|
9
|
+
// Ensure we're connected to the plugin
|
|
10
|
+
await connectPlugin();
|
|
11
|
+
const client = getPluginClient();
|
|
12
|
+
// Send IPC command via WebSocket to the mcp-bridge plugin
|
|
13
|
+
const response = await client.sendCommand({
|
|
14
|
+
command: 'invoke_tauri',
|
|
15
|
+
args: { command, args },
|
|
16
|
+
});
|
|
17
|
+
if (!response.success) {
|
|
18
|
+
return JSON.stringify({ success: false, error: response.error || 'Unknown error' });
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify({ success: true, result: response.data });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
return JSON.stringify({ success: false, error: message });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const GetWindowInfoSchema = z.object({});
|
|
28
|
+
export async function getWindowInfo() {
|
|
29
|
+
try {
|
|
30
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|get_window_info');
|
|
31
|
+
const parsed = JSON.parse(result);
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(parsed.result);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
throw new Error(`Failed to get window info: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Combined schema for managing IPC monitoring
|
|
43
|
+
export const ManageIPCMonitoringSchema = z.object({
|
|
44
|
+
action: z.enum(['start', 'stop']).describe('Action to perform: start or stop IPC monitoring'),
|
|
45
|
+
});
|
|
46
|
+
// Keep individual schemas for backward compatibility if needed
|
|
47
|
+
export const StartIPCMonitoringSchema = z.object({});
|
|
48
|
+
export const StopIPCMonitoringSchema = z.object({});
|
|
49
|
+
export async function manageIPCMonitoring(action) {
|
|
50
|
+
if (action === 'start') {
|
|
51
|
+
return startIPCMonitoring();
|
|
52
|
+
}
|
|
53
|
+
return stopIPCMonitoring();
|
|
54
|
+
}
|
|
55
|
+
export async function startIPCMonitoring() {
|
|
56
|
+
try {
|
|
57
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|start_ipc_monitor');
|
|
58
|
+
const parsed = JSON.parse(result);
|
|
59
|
+
if (!parsed.success) {
|
|
60
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(parsed.result);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Failed to start IPC monitoring: ${message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function stopIPCMonitoring() {
|
|
70
|
+
try {
|
|
71
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|stop_ipc_monitor');
|
|
72
|
+
const parsed = JSON.parse(result);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(parsed.result);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
throw new Error(`Failed to stop IPC monitoring: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const GetIPCEventsSchema = z.object({
|
|
84
|
+
filter: z.string().optional().describe('Filter events by command name'),
|
|
85
|
+
});
|
|
86
|
+
export async function getIPCEvents(filter) {
|
|
87
|
+
try {
|
|
88
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|get_ipc_events');
|
|
89
|
+
const parsed = JSON.parse(result);
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
92
|
+
}
|
|
93
|
+
let events = parsed.result;
|
|
94
|
+
if (filter && Array.isArray(events)) {
|
|
95
|
+
events = events.filter((e) => {
|
|
96
|
+
const event = e;
|
|
97
|
+
return event.command && event.command.includes(filter);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify(events);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
throw new Error(`Failed to get IPC events: ${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export const EmitTestEventSchema = z.object({
|
|
108
|
+
eventName: z.string(),
|
|
109
|
+
payload: z.unknown(),
|
|
110
|
+
});
|
|
111
|
+
export async function emitTestEvent(eventName, payload) {
|
|
112
|
+
try {
|
|
113
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|emit_event', {
|
|
114
|
+
eventName,
|
|
115
|
+
payload,
|
|
116
|
+
});
|
|
117
|
+
const parsed = JSON.parse(result);
|
|
118
|
+
if (!parsed.success) {
|
|
119
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
120
|
+
}
|
|
121
|
+
return JSON.stringify(parsed.result);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
throw new Error(`Failed to emit event: ${message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export const GetBackendStateSchema = z.object({});
|
|
129
|
+
export async function getBackendState() {
|
|
130
|
+
try {
|
|
131
|
+
const result = await executeIPCCommand('plugin:mcp-bridge|get_backend_state');
|
|
132
|
+
const parsed = JSON.parse(result);
|
|
133
|
+
if (!parsed.success) {
|
|
134
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
135
|
+
}
|
|
136
|
+
return JSON.stringify(parsed.result);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
throw new Error(`Failed to get backend state: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find an element using various selector strategies
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {string} params.selector - Element selector
|
|
6
|
+
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
7
|
+
*/
|
|
8
|
+
(function(params) {
|
|
9
|
+
const { selector, strategy } = params;
|
|
10
|
+
let element;
|
|
11
|
+
|
|
12
|
+
if (strategy === 'text') {
|
|
13
|
+
// Find element containing text
|
|
14
|
+
const xpath = "//*[contains(text(), '" + selector + "')]";
|
|
15
|
+
const result = document.evaluate(
|
|
16
|
+
xpath,
|
|
17
|
+
document,
|
|
18
|
+
null,
|
|
19
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
20
|
+
null
|
|
21
|
+
);
|
|
22
|
+
element = result.singleNodeValue;
|
|
23
|
+
} else if (strategy === 'xpath') {
|
|
24
|
+
// XPath selector
|
|
25
|
+
const result = document.evaluate(
|
|
26
|
+
selector,
|
|
27
|
+
document,
|
|
28
|
+
null,
|
|
29
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
30
|
+
null
|
|
31
|
+
);
|
|
32
|
+
element = result.singleNodeValue;
|
|
33
|
+
} else {
|
|
34
|
+
// CSS selector (default)
|
|
35
|
+
element = document.querySelector(selector);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (element) {
|
|
39
|
+
const outerHTML = element.outerHTML;
|
|
40
|
+
// Truncate long HTML to avoid overwhelming output
|
|
41
|
+
const truncated = outerHTML.length > 200
|
|
42
|
+
? outerHTML.substring(0, 200) + '...'
|
|
43
|
+
: outerHTML;
|
|
44
|
+
return 'Found element: ' + truncated;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return 'Element not found';
|
|
48
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus an element
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {string} params.selector - CSS selector for element to focus
|
|
6
|
+
*/
|
|
7
|
+
(function(params) {
|
|
8
|
+
const { selector } = params;
|
|
9
|
+
|
|
10
|
+
const element = document.querySelector(selector);
|
|
11
|
+
if (!element) {
|
|
12
|
+
throw new Error(`Element not found: ${selector}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
element.focus();
|
|
16
|
+
return `Focused element: ${selector}`;
|
|
17
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get computed CSS styles for elements
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {string} params.selector - CSS selector for element(s)
|
|
6
|
+
* @param {string[]} params.properties - Specific CSS properties to retrieve
|
|
7
|
+
* @param {boolean} params.multiple - Whether to get styles for all matching elements
|
|
8
|
+
*/
|
|
9
|
+
(function(params) {
|
|
10
|
+
const { selector, properties, multiple } = params;
|
|
11
|
+
|
|
12
|
+
const elements = multiple
|
|
13
|
+
? Array.from(document.querySelectorAll(selector))
|
|
14
|
+
: [document.querySelector(selector)];
|
|
15
|
+
|
|
16
|
+
if (!elements[0]) {
|
|
17
|
+
throw new Error(`Element not found: ${selector}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const results = elements.map(element => {
|
|
21
|
+
const styles = window.getComputedStyle(element);
|
|
22
|
+
|
|
23
|
+
if (properties.length > 0) {
|
|
24
|
+
const result = {};
|
|
25
|
+
properties.forEach(prop => {
|
|
26
|
+
result[prop] = styles.getPropertyValue(prop);
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Return all styles
|
|
32
|
+
const allStyles = {};
|
|
33
|
+
for (let i = 0; i < styles.length; i++) {
|
|
34
|
+
const prop = styles[i];
|
|
35
|
+
allStyles[prop] = styles.getPropertyValue(prop);
|
|
36
|
+
}
|
|
37
|
+
return allStyles;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return JSON.stringify(multiple ? results : results[0]);
|
|
41
|
+
})
|